package editor.code;

import editor.*;
import editor.headers.Header;
import editor.headers.HeaderComponent;
import editor.widgets.CustomScrollPane;
import editor.widgets.FindBar;
import editor.widgets.ObservedStringTable;
import editor.widgets.ToolTip;
import event.ListAddEvent;
import event.TextReplacementEvent;
import event.UndoSet;
import event.Undoable;
import model.Project;
import model.caret.CaretObserver;
import model.caret.CaretRef;
import model.caret.TextCaret;
import model.code.CodeFile;
import model.code.CodeFileChunk;
import model.code.Rowable;
import model.code.line.*;
import model.symbol.Symbol;
import observer.ObservedBoolean;
import observer.ObservedInt;
import observer.ObservedList;
import util.ClipboardUtil;
import util.StringUtil;
import widgets.CustomPopup;

import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

import static editor.Configuration.*;

/**
 * @author Neil Dickson
 */
public class LineField extends JComponent implements MouseListener, MouseMotionListener, KeyListener, FocusListener, CaretObserver, AncestorListener, ActionListener {
	protected	LineList<? extends Line>	lines;
	protected	boolean				isSingleLine;

	protected	CaretRef			caretRef;
	protected	Point				dragStartColAndLine;
	protected	Point				dragDestination;
	protected	Timer				caretBlinkTimer;
	protected	boolean				caretOn;

	protected	CustomPopup			tooltip;
	protected	AutoCompleteList	autocompleteList;

	protected	int					undoCombining;

	protected	Insets				insets;
	protected	int					rowHeight;

	protected static final int		UNDO_NONE		= 0;
	protected static final int		UNDO_CHARACTERS	= 1;
	protected static final int		UNDO_DELETIONS	= 2;

	protected static Color			BG_COLOUR				= Color.WHITE;
	public static Font				CODE_FONT				= new Font("Courier New",Font.PLAIN,12);
	public static Font				BOLD_FONT				= new Font("Courier New",Font.BOLD,12);
	public static FontMetrics		CODE_METRICS			= PwnIDE.editor.getFontMetrics(CODE_FONT);

	public static Color				SELECTION_BG_COLOUR		= new Color(0x000080);
	public static Color				SELECTION_TEXT_COLOUR	= new Color(0xFFFFFF);
	public static Color				SELECTION_HEAD_COLOUR	= Color.BLACK;
	public static int				SELECTION_HEAD_BLINKRATE= 500;	// Number of milliseconds between toggle
	public static Color				DRAG_HEAD_COLOUR		= new Color(0,0,0,128);

	public static int				LINE_HEIGHT				= (int)(CODE_FONT.getSize()*1.3);
	public static int				FONT_WIDTH				= 7;

	public LineField(LineList<? extends Line> lines,CaretRef caretRef,boolean isSingleLine) {
		this.lines = lines;
		this.caretRef = caretRef;
		this.isSingleLine = isSingleLine;
//		if (isSingleLine) {
//			while (lines.size()>1) {
//				lines.remove(lines.size()-1);
//			}
//		}
		dragStartColAndLine = null;
		insets = new Insets(0,2,2,2);
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
		addFocusListener(this);
		setFocusTraversalKeysEnabled(false);
		rowHeight = -1;

		caretRef.addObserver(this);

		addAncestorListener(this);

		setFont(CODE_FONT);
		updateSize();

		this.undoCombining = UNDO_NONE;

		setCursor(new Cursor(Cursor.TEXT_CURSOR));
	}

	/**
	 * Updates the size of this LineField, or repaints it, if this observes its {@link LineList} (default for {@link CodePanel}s)
	 * @param e the update notification
	 */
	public void actionPerformed(ActionEvent e) {
		if (e.getSource()==lines) {
			ensureValidCaret();
			Dimension dim = getMinimumSize();
			if (!dim.equals(getSize())) {
				updateSize();
			}
			else {
				repaint();
			}
		}
	}

	public Dimension getMinimumSize() {
		int maxWidth = Math.max(insets.left+insets.right,FONT_WIDTH*9);
		for (int i=0;i<lines.size();++i) {
			Line line = lines.get(i);
			maxWidth = Math.max(maxWidth,insets.left+insets.right+LineRenderer.getWidth(line));
		}
		return new Dimension(maxWidth,getRowHeight()*lines.size()+insets.top+insets.bottom);
	}
	public Dimension getPreferredSize() {
		return getMinimumSize();
	}
	protected Dimension updateSize() {
		// NOTE: ObservedStringTables should manage the sizes of their own LineFields
		if (getParent()!=null && !(getParent() instanceof ObservedStringTable.RowPanel) && !(getParent() instanceof ProjectOptionsDialog.SegmentTable.RowPanel)) {
			Dimension dim = getMinimumSize();
			if (!dim.equals(getSize())) {
				setSize(dim);
				setMinimumSize(dim);
				setPreferredSize(dim);
				// For some reason, providing Short.MAX_VALUE indicates that it can be any width, but left-aligns it in a managed layout... *sigh*
				setMaximumSize(new Dimension(Short.MAX_VALUE,dim.height+2));
	//			invalidate();
	//			revalidate();
			}
			return dim;
		}
		return getSize();
	}

	public LineList getLines() {
		return lines;
	}

	public int getRowHeight() {
		synchronized (this) {
			if (rowHeight==-1) {
				rowHeight = getFontMetrics(getFont()).getHeight();
				return rowHeight;
			}
			return rowHeight;
		}
	}

	public void setFont(Font font) {
		super.setFont(font);
		synchronized (this) {
			rowHeight = -1;
			getRowHeight();
		}
	}

	/**
	 * Gets the line and column number of the specified point, including insets
	 * @param xy
	 * @return
	 */
	public Point getColAndLine(Point xy) {
		int line = Math.max(0,Math.min(lines.size()-1,(xy.y-insets.top)/getRowHeight()));
		if (xy.x>=insets.left) {
			return new Point(LineRenderer.getColumn(lines.get(line),xy.x-insets.left),line);
		}
		return new Point(0,line);
	}

	/**
	 * Gets the x and y coordinates of the specified line and column number, including insets
	 * @param colAndLine
	 * @return
	 */
	public Point getXY(Point colAndLine) {
		return getXY(colAndLine.y,colAndLine.x);
	}
	public Point getXY(int line,int col) {
		int y = line*getRowHeight();
		if (col<0) {
			return new Point(0,y);
		}
		return new Point(LineRenderer.getX(lines.get((line<lines.size()) ? line : lines.size()-1),col)+insets.left,y+insets.top);
	}

	public void paintComponent(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		int rowHeight = getRowHeight();
		Rectangle clippingRect = g.getClipBounds();
		int startLine = (clippingRect!=null) ? Math.max(0,(clippingRect.y-insets.top)/rowHeight) : 0;
		int endLine = (clippingRect!=null) ? Math.min(lines.size(),(clippingRect.y+clippingRect.height-insets.top)/rowHeight + 1) : lines.size();
		g.setColor(BG_COLOUR);
		if (clippingRect!=null) {
			g.fillRect(clippingRect.x,clippingRect.y,clippingRect.width,clippingRect.height);
		}
		else {
			g.fillRect(0,0,getWidth(),getHeight());
		}
		Rectangle bounds = new Rectangle(insets.left,insets.top+startLine*rowHeight,getWidth()-insets.left,rowHeight);
		for (int i=startLine;i<endLine;++i) {
			if (caretRef.isTextCaret(lines)) {
				TextCaret caret = (TextCaret)caretRef.get();
				int[] selectedRange = caret.getSelectedRange(i);
				if (selectedRange==null) {
					LineRenderer.drawBackground(lines.get(i),g,bounds);
				}
				else {
					LineRenderer.drawBackground(lines.get(i),g,bounds,selectedRange[0],selectedRange[1]);
				}
			}
			else {
				LineRenderer.drawBackground(lines.get(i),g,bounds);
			}
			bounds.y += rowHeight;
		}
		if (lines.size()==1 && lines.get(0).length()==0){
			String emptyText = getEmptyText();
			bounds.y = insets.top;
			if (emptyText!=null) {
				LineRenderer.drawEmptyText(emptyText,g,bounds);
			}
		}
		else {
			// Draw the lines, but only bother with lines inside the clipping rectangle
			bounds.y = insets.top+startLine*rowHeight;
			for (int i=startLine;i<endLine;++i) {
				if (caretRef.isTextCaret(lines)) {
					TextCaret caret = (TextCaret)caretRef.get();
					int[] selectedRange = caret.getSelectedRange(i);
					if (selectedRange==null) {
						LineRenderer.draw(lines.get(i),g,bounds);
					}
					else {
						LineRenderer.draw(lines.get(i),g,bounds,selectedRange[0],selectedRange[1]);
					}
				}
				else {
					LineRenderer.draw(lines.get(i),g,bounds);
				}
				bounds.y += rowHeight;
			}
		}
		if (caretOn && caretRef.isTextCaret(lines)) {
			TextCaret caret = (TextCaret)caretRef.get();
			g.setColor(SELECTION_HEAD_COLOUR);
			g.fillRect(insets.left+LineRenderer.getX(lines.get(caret.getHeadLine()),caret.getHeadCol())-1,caret.getHeadLine()*getRowHeight(),2,getRowHeight());
		}
		if (dragStartColAndLine!=null && dragDestination!=null) {
			g.setColor(DRAG_HEAD_COLOUR);
			g.fillRect(insets.left+LineRenderer.getX(lines.get(dragDestination.y),dragDestination.x)-1,dragDestination.y*getRowHeight(),2,getRowHeight());
		}
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
	}

	/**
	 * @return the text to be displayed if this field is empty, or null if none
	 */
	public String getEmptyText() {
		if (getParent()!=null && getParent() instanceof FindBar) {
			FindBar parent = (FindBar)getParent();
			if (this==parent.findTextField) {
				return "(find)";
			}
			else {
				return "(replace)";
			}
		}
		else if (lines.get(0) instanceof SimpleLine) {
			if (lines.getLocationType()==CodeFileChunk.LOCATION_DESCRIPTION) {
				return "(Please enter a description.)";
			}
		}
		else if (lines instanceof TypeLine) {
			return "(data type)";
		}
		else if (lines instanceof ValueLine) {
			return (lines.getLocationType()==CodeFileChunk.LOCATION_MODIFIERS) ? "(offset)" : "(value)";
		}
		else if (lines instanceof NameLine) {
			return "(name)";
		}
		else if (lines instanceof SingleLine) {
			int location = lines.getLocationType();
			if (location==CodeFileChunk.LOCATION_AUTHORS) {
				return "(author)";
			}
			else if (location==CodeFileChunk.LOCATION_CONSTANTS_MEMBERS || location==CodeFileChunk.LOCATION_LOCAL_VARS || location==CodeFileChunk.LOCATION_PARAMETERS || location==CodeFileChunk.LOCATION_STRUCTURE_MEMBERS || location==CodeFileChunk.LOCATION_VAR_PIECES || location==CodeFileChunk.LOCATION_RETURNS) {
				return "(description)";
			}
			else if (location==CodeFileChunk.LOCATION_SEGMENT) {
				return "(segment)";
			}
			else if (location==CodeFileChunk.LOCATION_SEE_ALSO) {
				return "(reference)";
			}
			else {
				return "(empty)";
			}
		}
		return null;
	}

	public void mouseClicked(MouseEvent e) {
		try {
			if (e.getButton()==MouseEvent.BUTTON1 && e.getClickCount()==2) {
				Point colAndLine = getColAndLine(e.getPoint());
				int col = colAndLine.x;
				int lineNum = colAndLine.y;
				Line line = lines.get(lineNum);
				int token = line.getTokenAt(col);
				if (token>=0) {
					int tokenCol = line.getTokenColumn(token);
					int tokenEndCol = line.getTokenColumn(token+1);
					int x = LineRenderer.getX(line,tokenEndCol);
					if (caretRef.isTextCaret(lines)) {
						((TextCaret)(caretRef.get())).set(lineNum,tokenEndCol,lineNum,tokenCol,x);
					}
					else {
						caretRef.set(new TextCaret(caretRef,lines,lineNum,tokenEndCol,lineNum,tokenCol,x));
					}
					restartCaretBlink();
					repaint();
				}
			}
			else if (Configuration.GO_TO_DEFINITION.isTriggered(e)) {
				Symbol symbol = getSymbolAtPoint(e.getPoint());
				if (symbol!=null) {
					PwnIDE.ui.goToDefinition(symbol);
				}
			}
			// If the click was inside the caret, no dragging ever happened, so just empty the caret and end the drag
			else if (dragStartColAndLine!=null) {
				((TextCaret)caretRef.get()).set(dragStartColAndLine.y, dragStartColAndLine.x, getXY(dragStartColAndLine).x-insets.left);
				dragStartColAndLine = null;
				restartCaretBlink();
				repaint();
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while handling a mouse button being clicked.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	public void mousePressed(MouseEvent e) {
		try {
			Point colAndLine = getColAndLine(e.getPoint());
			Point xy = getXY(colAndLine);
			xy.x -= insets.left;
			if (Configuration.GO_TO_DEFINITION.isTriggered(e)) {
				Symbol symbol = getSymbolAtPoint(e.getPoint());
				if (symbol!=null) {
					PwnIDE.ui.goToDefinition(symbol);
				}
			}
			else if (!caretRef.isTextCaret(lines)) {
				caretRef.set(new TextCaret(caretRef,lines, colAndLine.y, colAndLine.x, xy.x));
			}
			else if (e.isShiftDown()) {
				((TextCaret)(caretRef.get())).setHead(colAndLine.y, colAndLine.x, xy.x);
			}
			else if (e.getButton()==MouseEvent.BUTTON1 || e.getButton()==MouseEvent.BUTTON3) {
				TextCaret caret = (TextCaret)caretRef.get();
				// Check if the position of the press is inside the caret (not before or after), to handle potential text dragging
				if (e.getButton()==MouseEvent.BUTTON1 && !caret.isEmpty() && !(
						colAndLine.y < caret.getFirstLine() || (colAndLine.y==caret.getFirstLine() && colAndLine.x < caret.getFirstLineCol()) ||
						colAndLine.y > caret.getLastLine() || (colAndLine.y==caret.getLastLine() && colAndLine.x > caret.getLastLineCol()))) {
					dragDestination = null;
					dragStartColAndLine = colAndLine;
				}
				else {
					caret.set(colAndLine.y, colAndLine.x, xy.x);
				}
			}
			requestFocus();
			repaint();
			undoCombining = UNDO_NONE;
			if (autocompleteList!=null) {
				autocompleteList.setVisible(false);
				autocompleteList = null;
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while handling a mouse button being pressed.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	public void mouseReleased(MouseEvent e) {
		try {
			Undoable event = null;
			if (e.getButton()==MouseEvent.BUTTON3) {
				// TODO: Handle right-click menu
				ErrorHandler.showNotImplementedMessage("Sorry, there's no right-click menu implemented yet.");
			}
			else if (e.getButton()==MouseEvent.BUTTON1 && dragStartColAndLine!=null) {
				Point colAndLine = getColAndLine(e.getPoint());
				Point xy = getXY(colAndLine);
				xy.x -= insets.left;
				TextCaret caret = (TextCaret)caretRef.get();
				// Check if the position is before the caret
				if (colAndLine.y < caret.getFirstLine() || (colAndLine.y==caret.getFirstLine() && colAndLine.x < caret.getFirstLineCol())) {
					// Calculate destination before the caret changes
					int newStartLine = colAndLine.y;
					int newStartCol = colAndLine.x;
					int newEndLine = newStartLine+(caret.getLastLine()-caret.getFirstLine());
					int newEndCol = (caret.getFirstLine()==caret.getLastLine()) ? (newStartCol+(caret.getLastLineCol()-caret.getFirstLineCol())) : caret.getLastLineCol();
					// The text was dragged backward, so remove the text, then add it at its earlier location.
					TextReplacementEvent removeEvent = lines.replace(caret.getFirstLine(),caret.getFirstLineCol(),caret.getLastLine(),caret.getLastLineCol(),"");
					TextReplacementEvent addEvent = lines.replace(colAndLine.y,colAndLine.x,colAndLine.y,colAndLine.x,removeEvent.getPreviousText());
					event = new UndoSet(new Undoable[]{removeEvent,addEvent},"Drag text in "+lines.getParent().getName());
					caret.set(newStartLine,newStartCol,newEndLine,newEndCol,xy.x);
				}
				// Check if the position is after the caret
				else if (colAndLine.y > caret.getLastLine() || (colAndLine.y==caret.getLastLine() && colAndLine.x > caret.getLastLineCol())) {
					// Calculate destination before the caret changes
					int newStartLine = caret.getFirstLine()+(colAndLine.y-caret.getLastLine());
					int newStartCol = (colAndLine.y==caret.getLastLine()) ? caret.getFirstLineCol()+(colAndLine.x-caret.getLastLineCol()) : colAndLine.x;
					int newEndLine = newStartLine+(caret.getLastLine()-caret.getFirstLine());
					int newEndCol = (caret.getFirstLine()==caret.getLastLine()) ? (newStartCol+(caret.getLastLineCol()-caret.getFirstLineCol())) : caret.getLastLineCol();
					// The text was dragged forward, so copy the text to its later location, then remove the original text.
					String replacedText = lines.getText(caret.getFirstLine(),caret.getFirstLineCol(),caret.getLastLine(),caret.getLastLineCol());
					TextReplacementEvent addEvent = lines.replace(colAndLine.y,colAndLine.x,colAndLine.y,colAndLine.x,replacedText);
					TextReplacementEvent removeEvent = lines.replace(caret.getFirstLine(),caret.getFirstLineCol(),caret.getLastLine(),caret.getLastLineCol(),"");
					event = new UndoSet(new Undoable[]{addEvent,removeEvent},"Drag text in "+lines.getParent().getName());
					caret.set(newStartLine,newStartCol,newEndLine,newEndCol,xy.x);
				}
				// If no dragging ever happened, the button was clicked, so just empty the caret and end the drag
				else if (dragDestination==null) {
					((TextCaret)caretRef.get()).set(dragStartColAndLine.y, dragStartColAndLine.x, getXY(dragStartColAndLine).x-insets.left);
				}
				dragStartColAndLine = null;
			}
			// TODO: Handle drag-and-drop stuff here
			// NOTE: Middle-button scrolling should be handled at a higher level
			restartCaretBlink();
			repaint();
			if (event!=null) {
				PwnIDE.editor.eventDone(event);
			}
			undoCombining = UNDO_NONE;
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while handling a mouse button being released.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}
	public void caretChanged(CaretRef ref) {
		if (caretRef.isTextCaret(lines)) {
			TextCaret caret = (TextCaret)caretRef.get();
			Point p = getXY(new Point(caret.getHeadCol(),caret.getHeadLine()));
			p.translate(getX(),getY());
			Container c = this.getParent();
			while (c!=null && !((c instanceof Header) && (((Header)c).getCode() instanceof CodeFile))) {
				p.translate(c.getX(),c.getY());
				c = c.getParent();
			}
			while (c!=null && !(c instanceof CustomScrollPane)) {
				c = c.getParent();
			}
			if (c!=null) {
				((CustomScrollPane)c).scrollToRectVisible(p.x-(FONT_WIDTH<<3),p.y-getRowHeight()*2,FONT_WIDTH<<4,getRowHeight()*8);
			}
			CodeFileChunk parent = lines.getParent();
			if (parent!=null) {
				Project project = parent.getProject();
				if (project!=null && project.getActiveFile()==parent.getFile()) {
					requestFocus();
				}
			}
		}
		repaint();
	}

	protected void restartCaretBlink() {
		caretOn = true;
		if (caretBlinkTimer!=null) {
			caretBlinkTimer.restart();
		}
		else {
			caretBlinkTimer = new Timer(SELECTION_HEAD_BLINKRATE,new ActionListener(){
				public void actionPerformed(ActionEvent e) {
					caretOn = !caretOn;
					repaint();
				}
			});
			caretBlinkTimer.start();
		}
	}

	/**
	 * This function makes sure that the {@link #caretRef} is valid (e.g. not past the end of the line).
	 * This is to be called upon notification of change in the text.
	 */
	protected void ensureValidCaret() {
		if (caretRef.isTextCaret(lines)) {
			TextCaret caret = (TextCaret)caretRef.get();
			int headLine = caret.getHeadLine();
			int headCol = caret.getHeadCol();
			int tailLine = caret.getTailLine();
			int tailCol = caret.getTailCol();
			headLine = Math.min(Math.max(0,headLine),lines.size()-1);
			tailLine = Math.min(Math.max(0,tailLine),lines.size()-1);
			headCol = Math.min(Math.max(0,headCol),lines.get(headLine).length());
			tailCol = Math.min(Math.max(0,tailCol),lines.get(tailLine).length());
			if (headLine!=caret.getHeadLine() || tailLine!=caret.getTailLine() || headCol!=caret.getHeadCol() || tailCol!=caret.getTailCol()) {
				caret.set(headLine,headCol,tailLine,tailCol,caret.getSavedX());
			}
		}
	}

	public void mouseEntered(MouseEvent e) {
//		throw new NotImplementedException();
	}

	public void mouseExited(MouseEvent e) {
		if (tooltip!=null) {
			tooltip.hide();
			tooltip = null;
		}
//		throw new NotImplementedException();
	}

	public void mouseDragged(MouseEvent e) {
		try {
			if (tooltip!=null) {
				tooltip.hide();
				tooltip = null;
			}
			if (caretRef.isTextCaret(lines) && dragStartColAndLine==null) {
				TextCaret caret = (TextCaret)caretRef.get();
				Point lineAndCol = getColAndLine(e.getPoint());
				Point xy = getXY(lineAndCol);
				caret.setHead(lineAndCol.y, lineAndCol.x, xy.x-insets.left);
				restartCaretBlink();
				repaint();
			}
			else if (dragStartColAndLine!=null) {
				dragDestination = getColAndLine(e.getPoint());
				repaint();
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, somehow an error occurred while handling dragging with the mouse.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nInventor IDE should still be stable unless other errors occur(red),\nbut you might want to save and restart the IDE to be safe.");
		}
	}

	public void mouseMoved(MouseEvent e) {
		try {
			if (tooltip!=null) {
				tooltip.hide();
				tooltip = null;
			}
			if (e.isControlDown()) {
				SyntaxError error = getErrorAtPoint(e.getPoint());
				if (error!=null) {
					Point p = e.getPoint();
					SwingUtilities.convertPointToScreen(p, this);
					p.x += 4;
					ToolTip tooltipComponent = new ToolTip(error);
					p.y -= tooltipComponent.getHeight()+4;
					tooltip = new CustomPopup(this,tooltipComponent,p);
					tooltip.show();
				}
				else {
					Symbol symbol = getSymbolAtPoint(e.getPoint());
					if (symbol!=null) {
						Point p = e.getPoint();
						SwingUtilities.convertPointToScreen(p, this);
						p.x += 4;
						ToolTip tooltipComponent = new ToolTip(symbol);
						p.y -= tooltipComponent.getHeight()+4;
						tooltip = new CustomPopup(this,tooltipComponent,p);
						tooltip.show();
					}
				}
			}
	//		System.out.println("Mouse Motion: "+e.getPoint());
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, somehow an error occurred while handling movement of the mouse.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nInventor IDE should still be stable unless other errors occur(red),\nbut you might want to save and restart the IDE to be safe.");
		}
	}

	public void keyTyped(KeyEvent e) {
		// NOTE: This is to fix a problem with Java, whereby all characters using Alt-Graph have no character in keyPressed but do in keyTyped.
		//       They also have isAltDown() return true and not isAltGraphDown() when in keyPressed.
		if (e.isAltGraphDown() && e.getKeyChar()>=' ' && e.getKeyChar()<='~') {
			keyPressed(e);
		}
	}

	public void keyPressed(KeyEvent e) {
		try {
			int newUndoCombining = UNDO_NONE;
			boolean killAutocomplete = (autocompleteList!=null && e.getKeyCode()!=KeyEvent.VK_CONTROL && e.getKeyCode()!=KeyEvent.VK_SHIFT && e.getKeyCode()!=KeyEvent.VK_ALT);
			if (caretRef.isTextCaret(lines)) {
				ObservedInt undoCombiningRef = new ObservedInt(newUndoCombining);
				ObservedBoolean killAutocompleteRef = new ObservedBoolean(killAutocomplete);
				Undoable event = keyPressedCommon(e,undoCombiningRef,killAutocompleteRef);
				newUndoCombining = undoCombiningRef.get();
				killAutocomplete = killAutocompleteRef.get();
				restartCaretBlink();
				repaint();
				if (event!=null) {
					PwnIDE.editor.eventDone(event);
				}
			}
			// NOTE: This must be outside the "if" so that the undoCombining is cleared when undo or redo is done
			undoCombining = newUndoCombining;
			if (killAutocomplete) {
				autocompleteList.setVisible(false);
				autocompleteList = null;
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while handling a key press.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	public Undoable keyPressedCommon(KeyEvent e, ObservedInt undoCombiningRef, ObservedBoolean killAutocompleteRef) {
		Undoable event = null;
		if (caretRef.isTextCaret(lines)) {
			TextCaret caret = (TextCaret)caretRef.get();
			// Character
			if (!e.isControlDown() && !e.isAltDown() && e.getKeyChar()>=' ' && e.getKeyChar()<='~') {
				TextReplacementEvent anEvent = replaceSelection(Character.toString(e.getKeyChar()));
				if (undoCombining==UNDO_CHARACTERS) {
					PwnIDE.editor.mergeWithUndoTop(anEvent);
				}
				else {
					event = anEvent;
				}
				undoCombiningRef.set(UNDO_CHARACTERS);
				if (autocompleteList!=null) {
					autocompleteList.setPopupLocation(getAutocompleteLocation(caret.getHeadLine(),caret.getHeadCol()));
					autocompleteList.setCurrentlyTyped(autocompleteList.getCurentlyTyped()+e.getKeyChar());
					killAutocompleteRef.set(false);
				}
			}
			// Move Cursor Up
			else if (UP.isTriggered(e)) {
				if (!isSingleLine) {
					if (autocompleteList!=null) {
						autocompleteList.selectedUp();
						killAutocompleteRef.set(false);
					}
					else if (caret.getHeadLine()>0) {
						Line prevLine = lines.get(caret.getHeadLine()-1);
						int column = LineRenderer.getColumn(prevLine,caret.getSavedX());
						caret.set(caret.getHeadLine()-1,column);
					}
					else {
						caret.set(caret.getHeadLine(),caret.getHeadCol());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					int row = parent.getRow();
					if (row>0) {
						LineField field = table.getCell(row-1,col);
						LineList lines = field.lines;
						int textCol = LineRenderer.getColumn(lines.get(0),caret.getSavedX());
						caret.set(lines,0,textCol,0,textCol,caret.getSavedX());
						field.requestFocus();
					}
				}
			}
			// Move Cursor Down
			else if (DOWN.isTriggered(e)) {
				if (!isSingleLine) {
					if (autocompleteList!=null) {
						autocompleteList.selectedDown();
						killAutocompleteRef.set(false);
					}
					else if (caret.getHeadLine()<lines.size()-1) {
						Line nextLine = lines.get(caret.getHeadLine()+1);
						int column = LineRenderer.getColumn(nextLine,caret.getSavedX());
						caret.set(caret.getHeadLine()+1,column);
					}
					else {
						caret.set(caret.getHeadLine(),caret.getHeadCol());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					int row = parent.getRow();
					if (row+1<table.getNumRows()) {
						LineField field = table.getCell(row+1,col);
						LineList lines = field.lines;
						int textCol = LineRenderer.getColumn(lines.get(0),caret.getSavedX());
						caret.set(lines,0,textCol,0,textCol,caret.getSavedX());
						field.requestFocus();
					}
				}
			}
			// Move Cursor 16 Lines Up
			else if (PAGE_UP.isTriggered(e)) {
				if (!isSingleLine) {
					if (autocompleteList!=null) {
						autocompleteList.selectedPageUp();
						killAutocompleteRef.set(false);
					}
					else if (caret.getHeadLine()>0) {
						int newLineNum = Math.max(0,caret.getHeadLine()-16);
						Line prevLine = lines.get(newLineNum);
						int column = LineRenderer.getColumn(prevLine,caret.getSavedX());
						caret.set(newLineNum,column);
					}
					else {
						caret.set(caret.getHeadLine(),caret.getHeadCol());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					LineField field = table.getCell(0,col);
					LineList lines = field.lines;
					int textCol = LineRenderer.getColumn(lines.get(0),caret.getSavedX());
					caret.set(lines,0,textCol,0,textCol,caret.getSavedX());
					field.requestFocus();
				}
			}
			// Move Cursor 16 Lines Down
			else if (PAGE_DOWN.isTriggered(e)) {
				if (!isSingleLine) {
					if (autocompleteList!=null) {
						autocompleteList.selectedPageDown();
						killAutocompleteRef.set(false);
					}
					else if (caret.getHeadLine()<lines.size()-1) {
						int newLineNum = Math.min(lines.size()-1,caret.getHeadLine()+16);
						Line nextLine = lines.get(newLineNum);
						int column = LineRenderer.getColumn(nextLine,caret.getSavedX());
						caret.set(newLineNum,column);
					}
					else {
						caret.set(caret.getHeadLine(),caret.getHeadCol());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					LineField field = table.getCell(table.getNumRows()-1,col);
					LineList lines = field.lines;
					int textCol = LineRenderer.getColumn(lines.get(0),caret.getSavedX());
					caret.set(lines,0,textCol,0,textCol,caret.getSavedX());
					field.requestFocus();
				}
			}
			// Insert Tab
			else if (TAB.isTriggered(e) && caret.getHeadLine()==caret.getTailLine()) {
				if (autocompleteList!=null) {
					String text = autocompleteList.getSelected();
					int lineNum = caret.getHeadLine();
					Point range = getAutcompleteColumnRange(lineNum,caret.getHeadCol(),true);
					int startCol = range.x;
					int endCol = range.y;
					event = lines.replace(lineNum,startCol,lineNum,endCol,text);
					String[] split = StringUtil.getLines(text);
					lineNum += split.length-1;
					if (split.length==1) {
						startCol += split[0].length();
					}
					else {
						startCol = split[split.length-1].length();
					}
					caret.set(lineNum,startCol,LineRenderer.getX(lines.get(lineNum),startCol));
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					int row = parent.getRow();
					LineField field = null;
					if (col+1<parent.getNumCols()) {
						field = parent.getField(col+1);
					}
					else if (row+1<table.getNumRows()) {
						field = table.getCell(row+1,0);
					}
					if (field!=null) {
						caret.set(field.lines,0,0,0,0,0);
						field.requestFocus();
					}
				}
				else {
					TextReplacementEvent anEvent = replaceSelection("\t");
					if (undoCombining==UNDO_CHARACTERS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					undoCombiningRef.set(UNDO_CHARACTERS);
				}
			}
			// Move caret to previous field
			else if (BACK_TAB.isTriggered(e) && getParent() instanceof ObservedStringTable.RowPanel) {
				ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
				ObservedStringTable table = (ObservedStringTable)parent.getParent();
				int col = parent.getCol(this);
				int row = parent.getRow();
				LineField field = null;
				if (col>0) {
					field = parent.getField(col-1);
				}
				else if (row>0) {
					field = table.getCell(row-1,parent.getNumCols()-1);
				}
				if (field!=null) {
					caret.set(field.lines,0,0,0,0,0);
					field.requestFocus();
				}
			}
			// Insert New Line
			else if (NEW_LINE.isTriggered(e)) {
				if (autocompleteList!=null) {
					String text = autocompleteList.getSelected();
					int lineNum = caret.getHeadLine();
					Point range = getAutcompleteColumnRange(lineNum,caret.getHeadCol(),false);
					int startCol = range.x;
					int endCol = range.y;
					event = lines.replace(lineNum,startCol,lineNum,endCol,text);
					String[] split = StringUtil.getLines(text);
					lineNum += split.length-1;
					if (split.length==1) {
						startCol += split[0].length();
					}
					else {
						startCol = split[split.length-1].length();
					}
					caret.set(lineNum,startCol,LineRenderer.getX(lines.get(lineNum),startCol));
				}
				else if (!isSingleLine) {
					String textToInsert = "\n";
					// Copy the whitespace from the current line to start the new line
					if (lines.get(caret.getHeadLine()).getNumReferences()>0 && (lines.get(caret.getHeadLine()).getReferences()[0]==Line.WHITE_SPACE_MARKER)) {
						int spaceEnd = lines.get(caret.getHeadLine()).getTokenColumn(1);
						String whiteSpace = new String(lines.get(caret.getHeadLine()).getText(),0,spaceEnd);
						if (spaceEnd<=caret.getHeadCol()) {
							textToInsert += whiteSpace;
						}
						else {
							textToInsert = whiteSpace.substring(caret.getHeadCol()) + textToInsert + whiteSpace.substring(0,caret.getHeadCol());
						}
					}
					TextReplacementEvent anEvent = replaceSelection(textToInsert);
					if (undoCombining==UNDO_CHARACTERS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					// NOTE: Don't set newUndoCombining to UNDO_CHARACTERS, so that line breaks will split the undos
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int row = parent.getRow();
					table.addRow(row+1,true);
				}
			}
			// Delete Selection or One Character Backward
			else if (DELETE_BACKWARD.isTriggered(e)) {
				TextReplacementEvent anEvent = null;
				if (!caret.isEmpty()) {
					anEvent = replaceSelection("");
				}
				else {
					Line line = lines.get(caret.getLine());
					if (caret.getCol()<=0) {
						if (caret.getLine()>0) {
							int currentLine = caret.getLine();
							int lastColumn = LineRenderer.getColumn(lines.get(currentLine-1),getWidth());
							// NOTE: The result of caret.getLine() may change as a result of function.replace()
							anEvent = lines.replace(currentLine-1,lastColumn,currentLine,0,"");
							int x = LineRenderer.getX(lines.get(currentLine-1),lastColumn);
							caret.set(currentLine-1,lastColumn,x);
						}
					}
					else {
						// When deleting the last character of a line, the caret automatically gets moved (to remain on the line),
						// so make sure that the same column number is referred to before and after the deletion.
						int col = caret.getCol();
						anEvent = lines.replace(caret.getLine(),col-1,caret.getLine(),col,"");
						int x = LineRenderer.getX(line,col-1);
						caret.set(caret.getLine(),col-1,x);
					}
				}
				if (anEvent!=null) {
					if (undoCombining==UNDO_DELETIONS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					undoCombiningRef.set(UNDO_DELETIONS);
				}
			}
			// Delete Selection or One Character Forward
			else if (DELETE_FORWARD.isTriggered(e)) {
				TextReplacementEvent anEvent = null;
				if (!caret.isEmpty()) {
					anEvent = replaceSelection("");
				}
				else {
					Line line = lines.get(caret.getLine());
					if (caret.getCol()>=line.length()) {
						if (caret.getLine()<lines.size()-1) {
							anEvent = lines.replace(caret.getLine(),line.length(),caret.getLine()+1,0,"");
							// Column and line don't change, but be sure to set x even though it won't have changed if it was set on this line & column previously
							int x = LineRenderer.getX(line,caret.getCol());
							caret.set(caret.getLine(),caret.getCol(),x);
						}
					}
					else {
						anEvent = lines.replace(caret.getLine(),caret.getCol(),caret.getLine(),caret.getCol()+1,"");
						// Column and line don't change, but be sure to set x even though it won't have changed if it was set on this line & column previously
						int x = LineRenderer.getX(line,caret.getCol());
						caret.set(caret.getLine(),caret.getCol(),x);
					}
				}
				if (anEvent!=null) {
					if (undoCombining==UNDO_DELETIONS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					undoCombiningRef.set(UNDO_DELETIONS);
				}
			}
			// Move Cursor Up and Select
			else if (!isSingleLine && SELECT_UP.isTriggered(e)) {
				if (caret.getHeadLine()>0) {
					Line prevLine = lines.get(caret.getHeadLine()-1);
					int column = LineRenderer.getColumn(prevLine,caret.getSavedX());
					caret.setHead(caret.getHeadLine()-1,column);
				}
			}
			// Move Cursor Down and Select
			else if (!isSingleLine && SELECT_DOWN.isTriggered(e)) {
				if (caret.getHeadLine()<lines.size()-1) {
					Line nextLine = lines.get(caret.getHeadLine()+1);
					int column = LineRenderer.getColumn(nextLine,caret.getSavedX());
					caret.setHead(caret.getHeadLine()+1,column);
				}
			}
			// Move Cursor Left
			else if (LEFT.isTriggered(e)) {
				if (caret.getHeadCol()>0) {
					Line line = lines.get(caret.getHeadLine());
					caret.set(caret.getHeadLine(),caret.getHeadCol()-1,LineRenderer.getX(line,caret.getHeadCol()-1));
				}
				else if (caret.getHeadLine()>0) {
					Line prevLine = lines.get(caret.getHeadLine()-1);
					int lastColumn = LineRenderer.getColumn(prevLine,getWidth());
					int x = LineRenderer.getX(prevLine,lastColumn);
					caret.set(caret.getHeadLine()-1,lastColumn,x);
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					int row = parent.getRow();
					LineField field = null;
					if (col>0) {
						field = parent.getField(col-1);
					}
					else if (row>0) {
						field = table.getCell(row-1,parent.getNumCols()-1);
					}
					if (field!=null) {
						LineList lines = field.lines;
						caret.set(lines,0,lines.get(0).length(),0,lines.get(0).length(),0);
						field.requestFocus();
					}
				}
				else if (getParent() instanceof FindBar) {
					FindBar parent = (FindBar)getParent();
					if (this==parent.replaceTextField) {
						LineField field = parent.findTextField;
						LineList lines = field.lines;
						caret.set(lines,0,lines.get(0).length(),0,lines.get(0).length(),0);
						field.requestFocus();
					}
				}
			}
			// Move Cursor Left and Select
			else if (SELECT_LEFT.isTriggered(e)) {
				if (caret.getHeadCol()>0) {
					Line line = lines.get(caret.getHeadLine());
					caret.setHead(caret.getHeadLine(),caret.getHeadCol()-1,LineRenderer.getX(line,caret.getHeadCol()-1));
				}
				else if (caret.getHeadLine()>0) {
					Line prevLine = lines.get(caret.getHeadLine()-1);
					int lastColumn = LineRenderer.getColumn(prevLine,getWidth());
					int x = LineRenderer.getX(prevLine,lastColumn);
					caret.setHead(caret.getHeadLine()-1,lastColumn,x);
				}
			}
			// Move Cursor Right
			else if (RIGHT.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				if (caret.getHeadCol()<lastColumn) {
					caret.set(caret.getHeadLine(),caret.getHeadCol()+1,LineRenderer.getX(line,caret.getHeadCol()+1));
				}
				else if (caret.getHeadLine()<lines.size()-1) {
					Line nextLine = lines.get(caret.getHeadLine()+1);
					int x = LineRenderer.getX(nextLine,0);
					caret.set(caret.getHeadLine()+1,0,x);
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int col = parent.getCol(this);
					int row = parent.getRow();
					if (col+1<parent.getNumCols()) {
						caret.set(parent.getField(col+1).lines,0,0,0,0,0);
						parent.getField(col+1).requestFocus();
					}
					else if (row+1<table.getNumRows()) {
						caret.set(table.getCell(row+1,0).lines,0,0,0,0,0);
						table.getCell(row+1,0).requestFocus();
					}
				}
				else if (getParent() instanceof FindBar) {
					FindBar parent = (FindBar)getParent();
					if (this==parent.findTextField) {
						LineField field = parent.replaceTextField;
						LineList lines = field.lines;
						caret.set(lines,0,0,0,0,0);
						field.requestFocus();
					}
				}
			}
			// Move Cursor Right and Select
			else if (SELECT_RIGHT.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				if (caret.getHeadCol()<lastColumn) {
					caret.setHead(caret.getHeadLine(),caret.getHeadCol()+1,LineRenderer.getX(line,caret.getHeadCol()+1));
				}
				else if (caret.getHeadLine()<lines.size()-1) {
					Line nextLine = lines.get(caret.getHeadLine()+1);
					int x = LineRenderer.getX(nextLine,0);
					caret.setHead(caret.getHeadLine()+1,0,x);
				}
			}
			// Move Cursor Left by One Word
			else if (LEFT_WORD.isTriggered(e)) {
				if (caret.getHeadCol()>0) {
					Line line = lines.get(caret.getHeadLine());
					int token = line.getTokenAt(caret.getHeadCol());
					if (caret.getHeadCol()>=line.length()) {
						token = line.getTokenAt(line.length()-1);
					}
					int col = line.getTokenColumn(token);
					if (col==caret.getHeadCol()) {
						token = line.getTokenAt(col-1);
						col = line.getTokenColumn(token);
					}
					caret.set(caret.getHeadLine(),col,LineRenderer.getX(line,col));
				}
				else if (caret.getHeadLine()>0) {
					Line prevLine = lines.get(caret.getHeadLine()-1);
					int lastColumn = LineRenderer.getColumn(prevLine,getWidth());
					int x = LineRenderer.getX(prevLine,lastColumn);
					caret.set(caret.getHeadLine()-1,lastColumn,x);
				}
			}
			// Move Cursor Left by One Word and Select
			else if (SELECT_LEFT_WORD.isTriggered(e)) {
				if (caret.getHeadCol()>0) {
					Line line = lines.get(caret.getHeadLine());
					int token = line.getTokenAt(caret.getHeadCol());
					if (caret.getHeadCol()>=line.length()) {
						token = line.getTokenAt(line.length()-1);
					}
					int col = line.getTokenColumn(token);
					if (col==caret.getHeadCol()) {
						token = line.getTokenAt(col-1);
						col = line.getTokenColumn(token);
					}
					caret.setHead(caret.getHeadLine(),col,LineRenderer.getX(line,col));
				}
				else if (caret.getHeadLine()>0) {
					Line prevLine = lines.get(caret.getHeadLine()-1);
					int lastColumn = LineRenderer.getColumn(prevLine,getWidth());
					int x = LineRenderer.getX(prevLine,lastColumn);
					caret.setHead(caret.getHeadLine()-1,lastColumn,x);
				}
			}
			// Move Cursor Right by One Word
			else if (RIGHT_WORD.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				if (caret.getHeadCol()<lastColumn) {
					int token = line.getTokenAt(caret.getHeadCol());
					int col = line.getTokenColumn(token+1);
					if (col==caret.getHeadCol()) {
						token = line.getTokenAt(col+1);
						col = line.getTokenColumn(token);
					}
					caret.set(caret.getHeadLine(),col,LineRenderer.getX(line,col));
				}
				else if (caret.getHeadLine()<lines.size()-1) {
					Line nextLine = lines.get(caret.getHeadLine()+1);
					int x = LineRenderer.getX(nextLine,0);
					caret.set(caret.getHeadLine()+1,0,x);
				}
			}
			// Move Cursor Right by One Word and Select
			else if (SELECT_RIGHT_WORD.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				if (caret.getHeadCol()<lastColumn) {
					int token = line.getTokenAt(caret.getHeadCol());
					int col = line.getTokenColumn(token+1);
					if (col==caret.getHeadCol()) {
						token = line.getTokenAt(col+1);
						col = line.getTokenColumn(token);
					}
					caret.setHead(caret.getHeadLine(),col,LineRenderer.getX(line,col));
				}
				else if (caret.getHeadLine()<lines.size()-1) {
					Line nextLine = lines.get(caret.getHeadLine()+1);
					int x = LineRenderer.getX(nextLine,0);
					caret.setHead(caret.getHeadLine()+1,0,x);
				}
			}
			// Delete Selection or One Word Backward
			else if (DELETE_WORD_BACKWARD.isTriggered(e)) {
				TextReplacementEvent anEvent = null;
				if (!caret.isEmpty()) {
					anEvent = replaceSelection("");
				}
				else {
					Line line = lines.get(caret.getLine());
					if (caret.getCol()<=0) {
						if (caret.getLine()>0) {
							int lastColumn = LineRenderer.getColumn(lines.get(caret.getLine()-1),getWidth());
							anEvent = lines.replace(caret.getLine()-1,lastColumn,caret.getLine(),0,"");
							int x = LineRenderer.getX(lines.get(caret.getLine()-1),lastColumn);
							caret.set(caret.getLine()-1,lastColumn,x);
						}
					}
					else {
						int token = line.getTokenAt(caret.getCol());
						if (caret.getCol()>=line.length()) {
							token = line.getTokenAt(line.length()-1);
						}
						int col = line.getTokenColumn(token);
						if (col==caret.getCol()) {
							token = line.getTokenAt(col-1);
							col = line.getTokenColumn(token);
						}
						anEvent = lines.replace(caret.getLine(),col,caret.getLine(),caret.getCol(),"");
						int x = LineRenderer.getX(line,col);
						caret.set(caret.getLine(),col,x);
					}
				}
				if (anEvent!=null) {
					if (undoCombining==UNDO_DELETIONS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					undoCombiningRef.set(UNDO_DELETIONS);
				}
			}
			// Delete Selection or One Word Forward
			else if (DELETE_WORD_FORWARD.isTriggered(e)) {
				TextReplacementEvent anEvent = null;
				if (!caret.isEmpty()) {
					anEvent = replaceSelection("");
				}
				else {
					Line line = lines.get(caret.getLine());
					if (caret.getCol()>=line.length()) {
						if (caret.getLine()<lines.size()-1) {
							anEvent = lines.replace(caret.getLine(),line.length(),caret.getLine()+1,0,"");
							// Column and line don't change, but be sure to set x even though it won't have changed if it was set on this line & column previously
							int x = LineRenderer.getX(line,caret.getCol());
							caret.set(caret.getLine(),caret.getCol(),x);
						}
					}
					else {
						int token = line.getTokenAt(caret.getCol());
						int col = line.getTokenColumn(token+1);
						if (col==caret.getCol()) {
							token = line.getTokenAt(col+1);
							col = line.getTokenColumn(token);
						}
						anEvent = lines.replace(caret.getLine(),caret.getCol(),caret.getLine(),col,"");
						// Column and line don't change, but be sure to set x even though it won't have changed if it was set on this line & column previously
						int x = LineRenderer.getX(line,caret.getCol());
						caret.set(caret.getLine(),caret.getCol(),x);
					}
				}
				if (anEvent!=null) {
					if (undoCombining==UNDO_DELETIONS) {
						PwnIDE.editor.mergeWithUndoTop(anEvent);
					}
					else {
						event = anEvent;
					}
					undoCombiningRef.set(UNDO_DELETIONS);
				}
			}
			// Go to definition
			else if (GO_TO_DEFINITION.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				Symbol symbol;
				if (caret.getTailLine() > caret.getHeadLine() || (caret.getTailLine()==caret.getHeadLine() && caret.getTailCol()>=caret.getHeadCol())) {
					symbol = line.getSymbolAt(caret.getHeadCol());
				}
				else {
					symbol = line.getSymbolAt(caret.getHeadCol()-1);
				}
				if (symbol!=null) {
					PwnIDE.ui.goToDefinition(symbol);
				}
			}
			// Move Cursor to End of Line
			else if (END.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				int x = LineRenderer.getX(line,lastColumn);
				caret.set(caret.getHeadLine(),lastColumn,x);
			}
			// Move Cursor to End of Line and Select
			else if (SELECT_END.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int lastColumn = LineRenderer.getColumn(line,getWidth());
				int x = LineRenderer.getX(line,lastColumn);
				caret.setHead(caret.getHeadLine(),lastColumn,x);
			}
			// Move Cursor to Beginning of Line
			else if (HOME.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int x = LineRenderer.getX(line,0);
				caret.set(caret.getHeadLine(),0,x);
			}
			// Move Cursor to Beginning of Line and Select
			else if (SELECT_HOME.isTriggered(e)) {
				Line line = lines.get(caret.getHeadLine());
				int x = LineRenderer.getX(line,0);
				caret.setHead(caret.getHeadLine(),0,x);
			}
			// Duplicate Lines or Selected Text
			else if (DUPLICATE.isTriggered(e)) {
				if (caret.isEmpty()) {
					if (!isSingleLine) {
						Line line = lines.get(caret.getLine());
						event = lines.insert(caret.getLine(),line.length(),"\n"+line.toString());
						int x = LineRenderer.getX(lines.get(caret.getLine()+1),caret.getCol());
						caret.set(caret.getLine()+1,caret.getCol(),x);
					}
					else if (getParent() instanceof ObservedStringTable.RowPanel) {
						ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
						ObservedStringTable table = (ObservedStringTable)parent.getParent();
						int row = parent.getRow();
						ObservedList<Rowable> elements = table.getElements();
						event = new ListAddEvent<Rowable>(elements,row+1);
						elements.add(row+1,elements.get(row).copy(table.getParentCode(),row+1));
						caret.set(table.getCell(row+1,0).lines,0,0,0,0,0);
						table.getCell(row+1,0).requestFocus();
					}
				}
				else if (caret.getHeadLine()==caret.getTailLine()) {
					Line line = lines.get(caret.getLine());
					int startCol = caret.getFirstLineCol();
					int endCol = caret.getLastLineCol();
					String text = line.toString(startCol,endCol);
					event = lines.insert(caret.getLine(),endCol,text);
					int x = LineRenderer.getX(lines.get(caret.getLine()),caret.getHeadCol());
					caret.set(caret.getLine(),caret.getHeadCol()+text.length(),caret.getLine(),caret.getTailCol()+text.length(),x);
				}
				else {
					int firstLine = caret.getFirstLine();
					int lastLine = caret.getLastLine();
					// Don't consider the last line if none of it is actually selected
					int lastLineConsidered = (caret.getLastLineCol()>0) ? lastLine : lastLine-1;
					int numLines = lastLineConsidered-firstLine+1;
					ArrayList<TextReplacementEvent> events = new ArrayList<TextReplacementEvent>(numLines);
					for (int i=firstLine;i<=lastLineConsidered;++i) {
						Line line = lines.get(i);
						Line prevLine = lines.get(i+numLines-1);
						events.add(lines.insert(i+numLines-1,prevLine.length(),"\n"+line.toString()));
					}
					if (events.size()==1) {
						event = events.get(0);
					}
					else {
						event = new UndoSet(events.toArray(new Undoable[events.size()]),"Duplicate Lines in "+lines.getParent().getName());
					}
					int x = LineRenderer.getX(lines.get(caret.getHeadLine()+numLines),caret.getHeadCol());
					caret.set(caret.getHeadLine()+numLines,caret.getHeadCol(),caret.getTailLine()+numLines,caret.getTailCol(),x);
				}
			}
			// Remove Line (just clear everything if single-line field)
			else if (REMOVE.isTriggered(e)) {
				if (caret.getLine()<lines.size()-1) {
					Line line = lines.get(caret.getLine());
					int x = LineRenderer.getX(line,caret.getCol());
					event = lines.replace(caret.getLine(),0,caret.getLine()+1,0,"");
					line = lines.get(caret.getLine());
					// Adjust to new columns
					int column = LineRenderer.getColumn(line,x);
					x = LineRenderer.getX(line,column);
					caret.set(caret.getLine(),column,x);
				}
				else if (!isSingleLine) {
					Line line = lines.get(caret.getLine());
					if (caret.getLine()>0) {
						event = lines.replace(caret.getLine()-1,lines.get(caret.getLine()-1).length(),caret.getLine(),line.length(),"");
					}
					else {
						event = lines.replace(caret.getLine(),0,caret.getLine(),line.length(),"");
					}
					int x = LineRenderer.getX(line,0);
					caret.set(caret.getLine(),0,x);
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int row = parent.getRow();
					table.removeRow(row,true);
				}
			}
			// Shift Line(s) Up
			else if (SHIFT_UP.isTriggered(e)) {
				if (!isSingleLine) {
					int firstLine = caret.getFirstLine();
					int lastLine = caret.getLastLine();
					// Don't consider the last line if none of it is actually selected
					int lastLineConsidered = (caret.getLastLineCol()>0 || firstLine==lastLine) ? lastLine : lastLine-1;
					if (firstLine>0) {
						String text = lines.get(firstLine-1).toString();
						Undoable removeEvent = lines.replace(firstLine-1,0,firstLine,0,"");
						Undoable addEvent = lines.insert(lastLineConsidered-1,lines.get(lastLineConsidered-1).length(),"\n"+text);
						event = new UndoSet(new Undoable[]{removeEvent,addEvent},"Shifted Line(s) Up in "+lines.getParent().getName());
						caret.set(caret.getHeadLine()-1,caret.getHeadCol(),caret.getTailLine()-1,caret.getTailCol(),caret.getSavedX());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int row = parent.getRow();
					if (row>0) {
						table.moveRowUp(row,true);
						parent.getField(parent.getCol(this)).requestFocus();
					}
				}
			}
			// Shift Line(s) Down
			else if (SHIFT_DOWN.isTriggered(e)) {
				if (!isSingleLine) {
					int firstLine = caret.getFirstLine();
					int lastLine = caret.getLastLine();
					// Don't consider the last line if none of it is actually selected
					int lastLineConsidered = (caret.getLastLineCol()>0 || firstLine==lastLine) ? lastLine : lastLine-1;
					if (lastLine<lines.size()-1) {
						String text = lines.get(lastLineConsidered+1).toString();
						Undoable removeEvent = lines.replace(lastLineConsidered,lines.get(lastLineConsidered).length(),lastLineConsidered+1,text.length(),"");
						Undoable addEvent = lines.insert(firstLine,0,text+"\n");
						event = new UndoSet(new Undoable[]{removeEvent,addEvent},"Shifted Line(s) Down in "+lines.getParent().getName());
						caret.set(caret.getHeadLine()+1,caret.getHeadCol(),caret.getTailLine()+1,caret.getTailCol(),caret.getSavedX());
					}
				}
				else if (getParent() instanceof ObservedStringTable.RowPanel) {
					ObservedStringTable.RowPanel parent = (ObservedStringTable.RowPanel)getParent();
					ObservedStringTable table = (ObservedStringTable)parent.getParent();
					int row = parent.getRow();
					if (row+1<table.getNumRows()) {
						table.moveRowDown(parent.getRow(),true);
						parent.getField(parent.getCol(this)).requestFocus();
					}
				}
			}
			// Select All
			else if (SELECT_ALL.isTriggered(e)) {
				caret.set(0,0,lines.size()-1,Integer.MAX_VALUE,LineRenderer.getX(lines.get(0),0));
			}
			// Paste Text
			else if (PASTE.isTriggered(e)) {
				String pastedText = ClipboardUtil.getClipboardText();
				if (pastedText!=null) {
					// NOTE: Elimination of extra lines done in replaceSelection.
					event = replaceSelection(pastedText);
				}
			}
			// Copy Text or Line
			else if (COPY.isTriggered(e)) {
				// If nothing selected, select and copy whole line
				if (caret.isEmpty()) {
					caret.set(caret.getHeadLine(),0,caret.getTailLine(),Integer.MAX_VALUE,LineRenderer.getX(lines.get(caret.getHeadLine()),0));
				}
				int firstLine = caret.getFirstLine();
				int lastLine = caret.getLastLine();
				int firstLineCol = caret.getFirstLineCol();
				int lastLineCol = caret.getLastLineCol();
				if (firstLine==lastLine) {
					String text = lines.get(firstLine).toString(firstLineCol,lastLineCol);
					if (lastLineCol==Integer.MAX_VALUE) {
						text += "\n";
					}
					ClipboardUtil.setClipboardText(text);
				}
				else {
					StringBuffer text = new StringBuffer(lines.get(firstLine).toString(firstLineCol,Integer.MAX_VALUE)).append('\n');
					for (int i=firstLine+1;i<lastLine;++i) {
						text.append(lines.get(i)).append('\n');
					}
					text.append(lines.get(lastLine).toString(-1,lastLineCol));
					if (lastLineCol==Integer.MAX_VALUE) {
						text.append('\n');
					}
					ClipboardUtil.setClipboardText(text.toString());
				}
			}
			// Cut Text or Line
			else if (CUT.isTriggered(e)) {
				// If nothing selected, select and cut whole line
				if (caret.isEmpty()) {
					Line line = lines.get(caret.getLine());
					String text = line.toString()+"\n";
					int x = LineRenderer.getX(line,caret.getCol());
					if (lines.size()==1) {
						event = lines.replace(0,0,0,line.length(),"");
						caret.set(0,0,LineRenderer.getX(line,0));
					}
					else {
						if (caret.getLine()<lines.size()-1) {
							event = lines.replace(caret.getLine(),0,caret.getLine()+1,0,"");
							line = lines.get(caret.getLine());
							int column = LineRenderer.getColumn(line,x);
							x = LineRenderer.getX(line,column);
							caret.set(caret.getLine(),column,x);
						}
						else {
							event = lines.replace(caret.getLine()-1,lines.get(caret.getLine()-1).length(),caret.getLine(),line.length(),"");
							line = lines.get(caret.getLine()-1);
							int column = LineRenderer.getColumn(line,x);
							x = LineRenderer.getX(line,column);
							caret.set(caret.getLine()-1,column,x);
						}
					}
					ClipboardUtil.setClipboardText(text);
				}
				else {
					// Otherwise, just like copy, but with a replaceSelection("") at the end
					int firstLine = caret.getFirstLine();
					int lastLine = caret.getLastLine();
					int firstLineCol = caret.getFirstLineCol();
					int lastLineCol = caret.getLastLineCol();
					if (firstLine==lastLine) {
						String text = lines.get(firstLine).toString(firstLineCol,lastLineCol);
						if (lastLineCol==Integer.MAX_VALUE) {
							text += "\n";
						}
						ClipboardUtil.setClipboardText(text);
					}
					else {
						StringBuffer text = new StringBuffer(lines.get(firstLine).toString(firstLineCol,Integer.MAX_VALUE)).append('\n');
						for (int i=firstLine+1;i<lastLine;++i) {
							text.append(lines.get(i)).append('\n');
						}
						text.append(lines.get(lastLine).toString(-1,lastLineCol));
						if (lastLineCol==Integer.MAX_VALUE) {
							text.append('\n');
						}
						ClipboardUtil.setClipboardText(text.toString());
					}
					event = replaceSelection("");
				}
			}
			else if (FIND.isTriggered(e)) {
				PwnIDE.ui.toFindBar((caret.isEmpty()) ? "" : lines.get(caret.getFirstLine()).toString(caret.getFirstLineCol(),(caret.getFirstLine()==caret.getLastLine()) ? caret.getLastLineCol() : Integer.MAX_VALUE));
			}
			updateSize();
		}
		return event;
	}

	public void keyReleased(KeyEvent e) {
		if (!e.isControlDown() && tooltip!=null) {
			tooltip.hide();
			tooltip = null;
		}
	}

	public void focusGained(FocusEvent e) {
		restartCaretBlink();
		repaint();
	}

	public void focusLost(FocusEvent e) {
		caretOn = false;
		if (caretBlinkTimer!=null) {
			caretBlinkTimer.stop();
		}
		if (tooltip!=null) {
			tooltip.hide();
			tooltip = null;
		}
		if (autocompleteList!=null) {
			autocompleteList.setVisible(false);
			autocompleteList = null;
		}
		repaint();
		undoCombining = UNDO_NONE;
		Component other = e.getOppositeComponent();
		if (other!=null && !(other instanceof LineField || other.getParent() instanceof HeaderComponent)) {
			PwnIDE.ui.updateFocus();
		}
	}

	protected Symbol getSymbolAtPoint(Point p) {
		Point colAndLine = getColAndLine(p);
		Line line = lines.get(colAndLine.y);
		return line.getSymbolAt(colAndLine.x);
	}
	protected SyntaxError getErrorAtPoint(Point p) {
		Point colAndLine = getColAndLine(p);
		Line line = lines.get(colAndLine.y);
		return line.getErrorAt(colAndLine.x);
	}

	/**
	 * Replaces the selection with text and moves the caret to after the inserted text.
	 * It properly handles single-line fields by cutting the input text down to just one line. 
	 * @param text the text with which to replace the selection
	 * @return the event corresponding to the replacement
	 */
	protected TextReplacementEvent replaceSelection(String text) {
		TextCaret caret = (TextCaret)caretRef.get();
		if (isSingleLine) {
			text = StringUtil.getFirstLine(text);
		}
		TextReplacementEvent event = lines.replace(caret.getFirstLine(),caret.getFirstLineCol(),caret.getLastLine(),caret.getLastLineCol(),text);
		caret.set(event.getLastLine(),event.getLastLineCol(),LineRenderer.getX(lines.get(event.getLastLine()),event.getLastLineCol()));
		return event;
	}


	public void ancestorAdded(AncestorEvent event) {
		caretRef.addObserver(this);
		updateSize();
	}

	public void ancestorRemoved(AncestorEvent event) {
		// NOTE: The tooltip can be present even when the focus wasn't in this component, so focusLost() wouldn't be called in that case
		if (tooltip!=null) {
			tooltip.hide();
			tooltip = null;
		}
		caretRef.removeObserver(this);
		undoCombining = UNDO_NONE;
	}

	public void ancestorMoved(AncestorEvent event) {}

	/**
	 * Calculates the location at which an {@link AutoCompleteList} is to appear
	 * @param line the line number
	 * @param column the column number
	 * @return a {@link Point} holding the location for the popup
	 */
	protected Point getAutocompleteLocation(int line,int column) {
		Point pt = getXY(new Point(column,line));
		pt.y -= AutoCompleteList.DEFAULT_VERTICAL_SPACE;
		SwingUtilities.convertPointToScreen(pt,this);
		return pt;
	}

	/**
	 * Calculates the range of columns to be replaced by an autocomplete at the specified line and column
	 * @param lineNum the line number
	 * @param column the column number
	 * @param isOverwriting true if the autocompletion is to overwrite the whole current {@link Token}
	 * @return a {@link Point} in which x holds the start column (inclusive) and y holds the end column (exclusive)
	 */
	protected Point getAutcompleteColumnRange(int lineNum,int column,boolean isOverwriting) {
		Line line = lines.get(lineNum);
		int numReferences = line.getNumReferences();
		int i = 0;
		int currentCol = 0;
		boolean isNonWordToken = true;
		if (column > 0) {
			for (;i<numReferences;++i) {
				if (column<=line.getTokenColumn(i+1)) {
					isNonWordToken = (line.getReferences()[i]==Line.WHITE_SPACE_MARKER || line.getReferences()[i]==Line.OPERATOR_MARKER);
					break;
				}
				currentCol = line.getTokenColumn(i+1);
			}
		}
		int startCol = (isNonWordToken) ? column : currentCol;
		int endCol = (isNonWordToken || !isOverwriting) ? column : line.getTokenColumn(i+1);
		return new Point(startCol,endCol);
	}
}